/** biome-ignore-all lint/suspicious/noExplicitAny: ignore any */

import { cn } from "@renderer/lib/utils";
import { EventNames, emitter } from "@renderer/services/event-service";
import type { ChatSettings } from "@shared/types/chat-settings";
import hardenReactMarkdown from "harden-react-markdown";
import { ScanSearch } from "lucide-react";
import type { ComponentProps } from "react";
import { memo, useMemo } from "react";
import type { Components } from "react-markdown";
import Markdown from "react-markdown";
import rehypeKatex from "rehype-katex";
import remarkCjkFriendly from "remark-cjk-friendly";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import { visit } from "unist-util-visit";
import { ButtonWithTooltip } from "../button-with-tooltip";
import { SyntaxHighlighter } from "../syntax-highlighter";
import { MarkdownCodeBlock } from "./markdown-code-block";
import { createComponents } from "./markdown-components";
import { MarkdownMathWrapper, processLaTeX } from "./markdown-math-wrapper";
import "katex/dist/katex.min.css";

/**
 * Parses markdown text and removes incomplete tokens to prevent partial rendering
 * of links, images, bold, and italic formatting during streaming.
 */
function parseIncompleteMarkdown(text: string): string {
  if (!text || typeof text !== "string") {
    return text;
  }

  let result = text;

  // Handle incomplete links and images
  // Pattern: [...] or ![...] where the closing ] is missing
  const linkImagePattern = /(!?\[)([^\]]*?)$/;
  const linkMatch = result.match(linkImagePattern);
  if (linkMatch) {
    // If we have an unterminated [ or ![, remove it and everything after
    const startIndex = result.lastIndexOf(linkMatch[1]);
    result = result.substring(0, startIndex);
  }

  // Handle incomplete bold formatting (**)
  const boldPattern = /(\*\*)([^*]*?)$/;
  const boldMatch = result.match(boldPattern);
  if (boldMatch) {
    // Count the number of ** in the entire string
    const asteriskPairs = (result.match(/\*\*/g) || []).length;
    // If odd number of **, we have an incomplete bold - complete it
    if (asteriskPairs % 2 === 1) {
      result = `${result}**`;
    }
  }

  // Handle incomplete italic formatting (__)
  const italicPattern = /(__)([^_]*?)$/;
  const italicMatch = result.match(italicPattern);
  if (italicMatch) {
    // Count the number of __ in the entire string
    const underscorePairs = (result.match(/__/g) || []).length;
    // If odd number of __, we have an incomplete italic - complete it
    if (underscorePairs % 2 === 1) {
      result = `${result}__`;
    }
  }

  // Handle incomplete single asterisk italic (*)
  const singleAsteriskPattern = /(\*)([^*]*?)$/;
  const singleAsteriskMatch = result.match(singleAsteriskPattern);
  if (singleAsteriskMatch) {
    // Count single asterisks that aren't part of **
    const singleAsterisks = result.split("").reduce((acc, char, index) => {
      if (char === "*") {
        // Check if it's part of a ** pair
        const prevChar = result[index - 1];
        const nextChar = result[index + 1];
        if (prevChar !== "*" && nextChar !== "*") {
          return acc + 1;
        }
      }
      return acc;
    }, 0);

    // If odd number of single *, we have an incomplete italic - complete it
    if (singleAsterisks % 2 === 1) {
      result = `${result}*`;
    }
  }

  // Handle incomplete single underscore italic (_)
  const singleUnderscorePattern = /(_)([^_]*?)$/;
  const singleUnderscoreMatch = result.match(singleUnderscorePattern);
  if (singleUnderscoreMatch) {
    // Count single underscores that aren't part of __
    const singleUnderscores = result.split("").reduce((acc, char, index) => {
      if (char === "_") {
        // Check if it's part of a __ pair
        const prevChar = result[index - 1];
        const nextChar = result[index + 1];
        if (prevChar !== "_" && nextChar !== "_") {
          return acc + 1;
        }
      }
      return acc;
    }, 0);

    // If odd number of single _, we have an incomplete italic - complete it
    if (singleUnderscores % 2 === 1) {
      result = `${result}_`;
    }
  }

  // Handle incomplete inline code blocks (`) - but avoid code blocks (```)
  const inlineCodePattern = /(`)([^`]*?)$/;
  const inlineCodeMatch = result.match(inlineCodePattern);
  if (inlineCodeMatch) {
    // Check if we're dealing with a code block (triple backticks)
    const allTripleBackticks = (result.match(/```/g) || []).length;

    // If we have an odd number of ``` sequences, we're inside an incomplete code block
    // In this case, don't complete inline code
    const insideIncompleteCodeBlock = allTripleBackticks % 2 === 1;

    if (!insideIncompleteCodeBlock) {
      // Count the number of single backticks that are NOT part of triple backticks
      let singleBacktickCount = 0;
      for (let i = 0; i < result.length; i++) {
        if (result[i] === "`") {
          // Check if this backtick is part of a triple backtick sequence
          const isTripleStart = result.substring(i, i + 3) === "```";
          const isTripleMiddle =
            i > 0 && result.substring(i - 1, i + 2) === "```";
          const isTripleEnd = i > 1 && result.substring(i - 2, i + 1) === "```";

          if (!isTripleStart && !isTripleMiddle && !isTripleEnd) {
            singleBacktickCount++;
          }
        }
      }

      // If odd number of single backticks, we have an incomplete inline code - complete it
      if (singleBacktickCount % 2 === 1) {
        result = `${result}\``;
      }
    }
  }

  // Handle incomplete strikethrough formatting (~~)
  const strikethroughPattern = /(~~)([^~]*?)$/;
  const strikethroughMatch = result.match(strikethroughPattern);
  if (strikethroughMatch) {
    // Count the number of ~~ in the entire string
    const tildePairs = (result.match(/~~/g) || []).length;
    // If odd number of ~~, we have an incomplete strikethrough - complete it
    if (tildePairs % 2 === 1) {
      result = `${result}~~`;
    }
  }

  return result;
}

// Create a hardened version of Markdown
const HardenedMarkdown = hardenReactMarkdown(Markdown);

const katexOptions = {
  strict: false,
  trust: true,
  throwOnError: false,
  macros: {
    "\\f": "f(#1)",
    "\\RR": "\\mathbb{R}",
    "\\NN": "\\mathbb{N}",
    "\\ZZ": "\\mathbb{Z}",
    "\\CC": "\\mathbb{C}",
    "\\QQ": "\\mathbb{Q}",
  },
  fleqn: false,
  leqno: false,
  output: "html",
  displayMode: true,
  errorColor: "#cc0000",
  minRuleThickness: 0.05,
  maxSize: Infinity,
  maxExpand: 1000,
  globalGroup: true,
  allowedEnvironments: [
    "matrix",
    "pmatrix",
    "bmatrix",
    "Bmatrix",
    "vmatrix",
    "Vmatrix",
    "equation",
    "equation*",
    "align",
    "align*",
    "gather",
    "gather*",
    "cases",
    "array",
    "split",
  ],
};

export interface MarkdownRendererProps {
  children: string;
  settings: ChatSettings;
  allowedImagePrefixes?: ComponentProps<
    ReturnType<typeof hardenReactMarkdown>
  >["allowedImagePrefixes"];
  allowedLinkPrefixes?: ComponentProps<
    ReturnType<typeof hardenReactMarkdown>
  >["allowedLinkPrefixes"];
  defaultOrigin?: ComponentProps<
    ReturnType<typeof hardenReactMarkdown>
  >["defaultOrigin"];
  parseIncompleteMarkdown?: boolean;
}

export const MarkdownRenderer = memo(
  ({
    children,
    settings,
    allowedImagePrefixes,
    allowedLinkPrefixes,
    defaultOrigin,
    parseIncompleteMarkdown: shouldParseIncompleteMarkdown = true,
  }: MarkdownRendererProps) => {
    const { disableMarkdown } = settings;

    // Parse the children to remove incomplete markdown tokens if enabled
    const parsedChildren = shouldParseIncompleteMarkdown
      ? parseIncompleteMarkdown(children)
      : children;

    const components: Components = useMemo(() => {
      const baseComponents = createComponents(settings);

      return {
        ...baseComponents,
        // Enhanced list styling
        ol: ({ node, children, className, ...props }) => (
          <ol
            className={cn(
              "ml-4 list-outside list-decimal space-y-1",
              className,
            )}
            {...props}
          >
            {children}
          </ol>
        ),
        ul: ({ node, children, className, ...props }) => (
          <ul
            className={cn("ml-4 list-outside list-disc space-y-1", className)}
            {...props}
          >
            {children}
          </ul>
        ),
        li: ({ node, children, className, ...props }) => (
          <li className={cn("py-1", className)} {...props}>
            {children}
          </li>
        ),
        // Enhanced heading styles with better typography
        h1: ({ node, children, className, ...props }) => (
          <h1
            className={cn("mt-6 mb-2 font-semibold text-3xl", className)}
            {...props}
          >
            {children}
          </h1>
        ),
        h2: ({ node, children, className, ...props }) => (
          <h2
            className={cn("mt-6 mb-2 font-semibold text-2xl", className)}
            {...props}
          >
            {children}
          </h2>
        ),
        h3: ({ node, children, className, ...props }) => (
          <h3
            className={cn("mt-6 mb-2 font-semibold text-xl", className)}
            {...props}
          >
            {children}
          </h3>
        ),
        h4: ({ node, children, className, ...props }) => (
          <h4
            className={cn("mt-6 mb-2 font-semibold text-lg", className)}
            {...props}
          >
            {children}
          </h4>
        ),
        h5: ({ node, children, className, ...props }) => (
          <h5
            className={cn("mt-6 mb-2 font-semibold text-base", className)}
            {...props}
          >
            {children}
          </h5>
        ),
        h6: ({ node, children, className, ...props }) => (
          <h6
            className={cn("mt-6 mb-2 font-semibold text-sm", className)}
            {...props}
          >
            {children}
          </h6>
        ),
        // Enhanced link styling with proper security attributes
        a: ({ node, children, className, ...props }) => (
          <a
            className={cn("font-medium text-primary underline", className)}
            rel="noreferrer"
            target="_blank"
            {...props}
          >
            {children}
          </a>
        ),
        // Enhanced strong text styling
        strong: ({ node, children, className, ...props }) => (
          <span className={cn("font-semibold", className)} {...props}>
            {children}
          </span>
        ),
        code: ({ children, className, node, ...rest }: any) => {
          const match = /language-(\w+)/.exec(className || "");
          const language = match?.[1] || "";
          const code = childrenToString(children);

          // Check if it's a code block by checking parent node or presence of newlines
          const hasNewlines = code.includes("\n");
          const hasClassname = Boolean(className?.startsWith("language-"));
          const isBlock = hasClassname || hasNewlines;

          // Keep Mermaid support via existing implementation
          if (isBlock && language === "mermaid") {
            return (
              <MarkdownCodeBlock
                className={className}
                language={language}
                settings={settings}
                {...rest}
              >
                {code}
              </MarkdownCodeBlock>
            );
          }

          if (!isBlock) {
            return (
              <code
                className={cn(
                  "font-mono [:not(pre)>&]:rounded-md [:not(pre)>&]:bg-bg/50 [:not(pre)>&]:px-1 [:not(pre)>&]:py-0.5",
                )}
                {...rest}
              >
                {children}
              </code>
            );
          }

          const canPreview = isPreviewableCode(language);
          const extraActions = (
            <>
              {canPreview && (
                <ButtonWithTooltip
                  title="Preview"
                  aria-label="Preview"
                  onPress={() =>
                    emitter.emit(EventNames.CODE_PREVIEW_OPEN, {
                      code,
                      language,
                    })
                  }
                >
                  <ScanSearch className="h-3.5 w-3.5" />
                </ButtonWithTooltip>
              )}
            </>
          );

          return (
            <div className="my-4 overflow-hidden rounded-xl border border-border/30">
              <SyntaxHighlighter
                title={language || "code"}
                language={language || "text"}
                showLineNumbers={false}
                className={cn("text-sm", "h-auto")}
                showCollapseIcon
                defaultCollapsed={Boolean(settings?.collapseCodeBlock)}
                collapseMode="height"
                collapsedHeight={160}
                extraActions={extraActions}
              >
                {code}
              </SyntaxHighlighter>
            </div>
          );
        },
        span: ({ className, node, ...props }) => {
          if (className === "katex") {
            return (
              <MarkdownMathWrapper>
                <span className={className} {...props} />
              </MarkdownMathWrapper>
            );
          }

          return <span className={className} {...props} />;
        },
      };
    }, [settings]);

    if (disableMarkdown) {
      return <div className="whitespace-pre-wrap">{parsedChildren}</div>;
    }

    return (
      <div className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
        <HardenedMarkdown
          remarkPlugins={[
            remarkGfm,
            remarkHighlight,
            remarkCjkFriendly,
            [remarkMath, { singleDollar: true, doubleBackslash: true }],
          ]}
          rehypePlugins={[
            [
              rehypeKatex,
              {
                ...katexOptions,
                output: "htmlAndMathml",
                trust: true,
                strict: false,
                throwOnError: true,
              },
            ],
          ]}
          components={components}
          allowedImagePrefixes={allowedImagePrefixes ?? ["*"]}
          allowedLinkPrefixes={allowedLinkPrefixes ?? ["*"]}
          defaultOrigin={defaultOrigin}
        >
          {processLaTeX(parsedChildren)}
        </HardenedMarkdown>
      </div>
    );
  },
  (prevProps, nextProps) => prevProps.children === nextProps.children,
);

function childrenToString(children: unknown): string {
  if (children == null) return "";
  if (typeof children === "string") return children;
  if (typeof children === "number" || typeof children === "boolean")
    return String(children);
  if (Array.isArray(children))
    return children.map((c) => childrenToString(c)).join("");
  // Fallback for React elements-like objects
  // @ts-expect-error accessing props for potential ReactElement
  const nested = children?.props?.children;
  if (nested != null) return childrenToString(nested);
  return "";
}

function isPreviewableCode(language: string): boolean {
  const lang = language.toLowerCase();
  if (lang === "html") return true;
  if (lang === "svg") return true;
  return false;
}

function remarkHighlight() {
  return (tree: any) => {
    visit(tree, "text", (node, index, parent) => {
      const matches = Array.from(
        node.value.matchAll(/==(.*?)==/g),
      ) as Array<RegExpMatchArray>;
      if (!matches.length) return;

      const children: any[] = [];
      let lastIndex = 0;

      matches.forEach((match: RegExpMatchArray) => {
        const beforeText = node.value.slice(lastIndex, match.index);
        if (beforeText) {
          children.push({ type: "text", value: beforeText });
        }

        children.push({
          type: "highlight",
          data: { hName: "mark" },
          children: [{ type: "text", value: match[1] }],
        });

        lastIndex = (match.index ?? 0) + match[0].length;
      });

      const afterText = node.value.slice(lastIndex);
      if (afterText) {
        children.push({ type: "text", value: afterText });
      }

      parent.children.splice(index, 1, ...children);
    });
  };
}
